using System;
using System.IO;
using System.Collections.Generic;
using WDFactoryLib;

namespace CardsEngine
{

	public abstract class CardGame: FactoryUser
	{
		public class Args
		{
			private CardGame game;

			public Args(CardGame _game)
			{
				game = _game;
			}

			/// <summary>
			/// Gets the CardGame associated with this instance
			/// (Public Read-Only; Protected Read-Write)
			/// </summary>
			/// <value>
			/// The CardGame
			/// </value>
			public CardGame Game {
				get { return game; }
			}
		}

		public event Action<Args> GameStarting;
		public event Action<Args> GameEnding;

		private ICollection<CardsPlayer> players;
		private Stack<Card> cardDeck;
		private IDeckGenerator deckGenerator;
		private bool inProgress;

		private TextWriter debugStream = Console.Out;

		/// <summary>
		/// A value of 'true' enables debug output for this instance
		/// </summary>
		public bool Debug = false; // set to true from anywhere to enable debug output for all members

		// Prefix for all debug messages
		// TODO - Security: Make sure this string is free from injects {0} {1} etc by using reflection
		private static string debugMessagePrefix = "DBG(CardGame): ";

		public CardGame ()
            :base()
		{
			InitializeInstance();
		}

        public CardGame(IDeckGenerator _deckGenerator)
            : base()
        {
            InitializeInstance(_deckGenerator);
        }

		public CardGame (IFactory _factory)
            :base(_factory)
		{
			InitializeInstance();
		}

		public CardGame (IFactory _factory, IDeckGenerator _deckGenerator)
            :base(_factory)
		{
			InitializeInstance(_deckGenerator);
		}

		private void InitializeInstance (IDeckGenerator deck_generator = null)
		{
			players = new SortedSet<CardsPlayer>();

            if (deck_generator == null)
                deckGenerator = TestKit.BasicDeckGenerator.Default;
            else
                deckGenerator = deck_generator;

			inProgress = false;
			ResetDeck ();

			// Prevent NullReferenceException in case no other methods are ever bound to the events
			GameStarting += DoNothing;
			GameEnding += DoNothing;
		}

		public static void DoNothing(Args a)
		{ }

		protected void DebugWrite (string msg, params object[] args)
		{
			if (Debug)
				debugStream.Write(debugMessagePrefix + msg,args);
		}

		protected void DebugWriteLine ()
		{
			if (Debug)
				debugStream.WriteLine ();
		}

		protected void DebugWriteLine (string msg, params object[] args)
		{
			if (Debug)
				debugStream.WriteLine(msg,args);
		}

		/// <summary>
		/// Gets a value indicating whether this <see cref="CardsEngine.CardGame"/> is in progress.
		/// </summary>
		/// <value>
		/// <c>true</c> if in progress; otherwise, <c>false</c>.
		/// </value>
		public bool InProgress { get { return inProgress; } }

		/// <summary>
		/// (Write-Only) Sets the stream for debug output,
		/// only applies if Debug == true
		/// </summary>
		/// <value>
		/// The debug stream.
		/// </value>
		public TextWriter DebugStream { set { debugStream = value; } }

		protected ICollection<CardsPlayer> Players { get { return players; } }
		protected Stack<Card> Deck { get { return cardDeck; } }

        /// <summary>
        /// Gets a shallow copy of the collection containing
        /// the players in this card game, and return the result as an array
        /// </summary>
        /// <returns></returns>
        public CardsPlayer[] GetCopyOfPlayers()
        {
            CardsPlayer[] result = new CardsPlayer[Players.Count];

            Players.CopyTo(result, 0);

            return result;
        }

        /// <summary>
        /// Adds a player to the game. May not be called while the game is in progress.
        /// </summary>
        /// <exception cref="System.InvalidOperationException">Thrown if the game is already in progress when this method is called</exception>
        /// <param name="p">The player to add</param>
        public void AddPlayer(Player p)
        {
            if (InProgress)
                throw new InvalidOperationException("Cannot add a player to the game while the game is in progress");

            players.Add(new CardsPlayer(p));
        }

        /// <summary>
        /// Removes a player from the game. May not be called while the game is in progress.
        /// </summary>
        /// <exception cref="System.InvalidOperationException">Thrown if the game is already in progress when this method is called</exception>
        /// <param name="p">The player to remove</param>
        public void RemovePlayer(CardsPlayer p)
        {
            if (InProgress)
                throw new InvalidOperationException("Cannot remove a player from the game while the game is in progress");

            players.Remove(p);
        }

        /// <summary>
        /// Removes a player from the game. May not be called while the game is in progress.
        /// </summary>
        /// <exception cref="System.InvalidOperationException">Thrown if the game is already in progress when this method is called</exception>
        /// <exception cref="System.ArgumentException">Thrown if there is no CardsPlayer object in this instance's collection whose owner is the specified Player (p)</exception>
        /// <param name="p">The player to remove</param>
        public void RemovePlayer(Player p)
        {
            if (InProgress)
                throw new InvalidOperationException("Cannot remove a player from the game while the game is in progress");

            foreach (CardsPlayer cp in Players)
            {
                if (cp.Owner == p)
                {
                    players.Remove(cp);
                    return;
                }
            }

            throw new ArgumentException("No CardsPlayer having the specified owner could be found in my collection");
        }

		protected void ResetDeck()
		{
			DebugWrite("Resetting deck contents... ");
			cardDeck = new Stack<Card>(deckGenerator.GenerateShuffledDeck());
			DebugWriteLine("Done");
		}

		protected Card DealCardToPlayer(CardsPlayer p)
		{
			Card tmp = cardDeck.Pop ();
			p.Hand.Add (tmp);

			return tmp;
		}

		/// <summary>
		/// Starts the game - performed between
		/// </summary>
		protected abstract void StartGame();
		protected abstract void EndGame();

		/// <summary>
		/// Obtain a new instance of the class to be used as arguments
		/// for the GameStarting and GameStarted events
		/// </summary>
		/// <returns>
		/// The game starting arguments.
		/// </returns>
		public virtual Args GetGameStartArgs()
		{
			return new Args(this);
		}

		/// <summary>
		/// Obtain a new instance of the class to be used as arguments
		/// for the GameEnding and GameEnded events
		/// </summary>
		/// <returns>
		/// The game over arguments.
		/// </returns>
		public virtual Args GetGameEndArgs ()
		{
			return new Args(this);
		}

		public void BeginGame ()
		{
			Args myArgs = GetGameStartArgs();
			inProgress = true;
			InvokeGameStartingEvent(myArgs);
			StartGame ();
		}

		public void FinishGame ()
		{
			Args myArgs = GetGameEndArgs();
			inProgress = false;
			InvokeGameEndingEvent(myArgs);
			EndGame();
		}

		private void InvokeGameEndingEvent (Args arg)
		{
			try {
				GameEnding.Invoke(arg);
			} catch (Exception ex)
			{
				DebugWriteLine("An exception occurred while invoking the GameEnding event:\n{0}",ex);
				throw new ApplicationException("Failed to invoke the CardGame.GameEnding event",ex);
			}
		}

		private void InvokeGameStartingEvent(Args arg)
		{
			try {
				GameStarting.Invoke(arg);
			} catch (Exception ex)
			{
				DebugWriteLine("An exception occurred while invoking the GameEnding event:\n{0}",ex);
				throw new ApplicationException("Failed to invoke the CardGame.GameEnding event",ex);
			}
		}
	}
}

